package net.xo68.destiny.manager.service.impl;

import lombok.extern.slf4j.Slf4j;
import net.xo68.destiny.contract.domain.job.TriggerParameter;
import net.xo68.destiny.contract.domain.job.JobDetailExtends;
import net.xo68.destiny.contract.enums.job.JobInstanceStateEnum;
import net.xo68.destiny.contract.enums.trigger.JobTriggerTypeEnum;
import net.xo68.destiny.core.cron.CronExpression;
import net.xo68.destiny.core.util.TimeUtils;
import net.xo68.destiny.manager.entity.*;
import net.xo68.destiny.manager.mapper.*;
import net.xo68.destiny.starter.common.sched.DestinyTimingWheelScheduler;
import net.xo68.destiny.starter.manager.sched.JobTriggerObserverProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.ParseException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 任务触发观察提供者实现
 * @author wuxie
 * @version 2020-12-27
 */
@Slf4j
@Service
public class JobTriggerObserverProviderImpl implements JobTriggerObserverProvider {

    @Autowired
    private TriggerMapper triggerMapper;
    @Autowired
    private JobDetailMapper jobDetailMapper;
    @Autowired
    private SimpleTriggerMapper simpleTriggerMapper;
    @Autowired
    private CronTriggerMapper cronTriggerMapper;
    @Autowired
    private JobDetailExtendMapper jobDetailExtendMapper;
    @Autowired
    private JobInstanceMapper jobInstanceMapper;
    @Autowired
    private DestinyTimingWheelScheduler destinyTimingWheelScheduler;

    @Override
    public void oberveJob(Runnable runnable, long bufferTimeSecond, int triggerObserverMaxSize) {
        //log.debug("bufferTimeSecond:{}, triggerObserverMaxSize:{}",bufferTimeSecond, triggerObserverMaxSize);
        final List<Trigger> triggers=triggerMapper.waitObservelist(bufferTimeSecond,triggerObserverMaxSize);
        //log.debug("triggers: {}",triggers);

        //取对应的数据
        final List<Long> allJobIds= new ArrayList<>();
        final List<Long> simpleJobIds= new ArrayList<>();
        final List<Long> cronJobIds= new ArrayList<>();
        triggers.forEach(trigger -> {
            try{
                JobTriggerTypeEnum jobTriggerTypeEnum=JobTriggerTypeEnum.valueOf(trigger.getTriggerType());
                allJobIds.add(trigger.getJobId());
                switch (jobTriggerTypeEnum){
                    case SIMPLE:
                        simpleJobIds.add(trigger.getJobId());
                        break;
                    case CRON:
                        cronJobIds.add(trigger.getJobId());
                        break;
                }
            }catch (Exception e){
                log.error(e.getMessage(), e);
            }

        });
        final List<JobDetail> jobDetails = allJobIds.isEmpty() ? new ArrayList<>() : jobDetailMapper.listByJobs(allJobIds);
        final List<SimpleTrigger> simpleTriggers = simpleJobIds.isEmpty() ? new ArrayList<>() : simpleTriggerMapper.listByJobs(simpleJobIds);
        //log.debug("simpleTriggers: {}",simpleTriggers);
        final List<CronTrigger> cronTriggers = cronJobIds.isEmpty() ? new ArrayList<>() : cronTriggerMapper.listByJobs(cronJobIds);
        //log.debug("cronTriggers: {}",cronTriggers);

        //进行处理
        LocalDateTime currentTime= TimeUtils.nowLocalDateTime();
        LocalDateTime minTime= currentTime.plusSeconds(bufferTimeSecond*-1L);
        LocalDateTime maxTime=currentTime.plusSeconds(bufferTimeSecond);
        for (final Trigger trigger:triggers) {
            if(trigger.getNextFireTime()==null){
                // < min
                calculateAndExecuteAfterTrigger(jobDetails,simpleTriggers,cronTriggers,trigger,TimeUtils.nowLocalDateTime(),maxTime);
                continue;
            }
            LocalDateTime fireTime=TimeUtils.ofLocalDateTime(trigger.getNextFireTime());
            if(fireTime.isAfter(currentTime) || fireTime.isEqual(currentTime)){
                //now,now + bufferTimeSecond,, 触发任务，更新上次触发时间，计算下次触发时间
                fireJob(jobDetails,trigger);
                trigger.setPrevFireTime(trigger.getNextFireTime());
                calculateAndExecuteAfterTrigger(jobDetails,simpleTriggers,cronTriggers,trigger,TimeUtils.ofLocalDateTime(trigger.getNextFireTime()),maxTime);
            }else if(fireTime.isAfter(minTime)){
                //min,now,, 更新下次触发时间为当前， 触发任务，更新上次触发时间，计算下次触发时间
                trigger.setNextFireTime(TimeUtils.nowDate());
                fireJob(jobDetails,trigger);
                trigger.setPrevFireTime(trigger.getNextFireTime());
                calculateAndExecuteAfterTrigger(jobDetails,simpleTriggers,cronTriggers,trigger,TimeUtils.ofLocalDateTime(trigger.getNextFireTime()),maxTime);
            }else {
                // < min ，，计算下次触发时间，如果触发时间在缓冲窗口区时触发任务
                calculateAndExecuteAfterTrigger(jobDetails,simpleTriggers,cronTriggers,trigger,TimeUtils.nowLocalDateTime(),maxTime);
            }
        }

        //更新触发器的前一次和下一次触发时间
        if(!triggers.isEmpty()){
            triggerMapper.updateFireTime(triggers);
        }


    }

    /**
     * 计算触发时间并将后面合适的任务推入时光轮
     * @param jobDetails
     * @param simpleTriggers
     * @param cronTriggers
     * @param trigger
     */
    private void calculateAndExecuteAfterTrigger(final List<JobDetail> jobDetails, final List<SimpleTrigger> simpleTriggers, final List<CronTrigger> cronTriggers, final Trigger trigger, LocalDateTime fromTime, LocalDateTime maxTime){
        JobTriggerTypeEnum jobTriggerTypeEnum=JobTriggerTypeEnum.SIMPLE;
        try{
            jobTriggerTypeEnum=JobTriggerTypeEnum.valueOf(trigger.getTriggerType());
        }catch (Exception e){
            log.error(e.getMessage(),e);
            return;
        }

        LinkedList<LocalDateTime> trggerTimes=new LinkedList<>();
        LocalDateTime nextTime =fromTime;
        //log.debug("next:{}, max: {}",nextTime, maxTime);

        switch (jobTriggerTypeEnum){
            case SIMPLE:
                //简单触发器
                final SimpleTrigger simpleTrigger=simpleTriggers.stream().filter(st -> {
                    return st.getJobId().equals(trigger.getJobId());
                }).findFirst().orElse(null);
                if(simpleTrigger==null){
                    break;
                }
                while (nextTime.isBefore(maxTime) || nextTime.isEqual(maxTime)) {
                    nextTime = nextTime.plusSeconds(simpleTrigger.getRepeatInterval());
                    if (nextTime != null) {
                        trggerTimes.add(nextTime);
                    } else {
                        break;
                    }
                }
                break;
            case CRON:
                //表达式触发器
                CronTrigger cronTrigger = cronTriggers.stream().filter(ct -> {
                    return ct.getJobId().equals(trigger.getJobId());
                }).findFirst().orElse(null);
                if(cronTrigger==null){
                    break;
                }
                try {
                    CronExpression cronExpression=new CronExpression(cronTrigger.getCronExpression());
                    while (nextTime.isBefore(maxTime) || nextTime.isEqual(maxTime)) {
                        nextTime = cronExpression.getNextValidTimeAfter(nextTime);
                        if (nextTime != null) {
                            trggerTimes.add(nextTime);
                        } else {
                            break;
                        }
                    }
                } catch (ParseException e) {
                    log.error(e.getMessage(), e);
                }
                break;
        }

        for (LocalDateTime trggerTime : trggerTimes) {
            if (trggerTime.isBefore(maxTime)) {
                trigger.setNextFireTime(TimeUtils.ofDate(trggerTime));
                fireJob(jobDetails,trigger);
                trigger.setPrevFireTime(trigger.getNextFireTime());
            } else {
                trigger.setNextFireTime(TimeUtils.ofDate(trggerTime));
            }
        }
    }

    /**
     * 预触发任务，推入时光轮预调度
     * @param jobDetails
     * @param trigger
     */
    private void fireJob(final List<JobDetail> jobDetails,final Trigger trigger){
        JobDetail jobDetail=jobDetails.stream().filter(jd -> {
            return jd.getJobId().equals(trigger.getJobId());
        }).findFirst().orElse(null);
        if(jobDetail==null){
            log.error("{}","未找到任务关联信息");
            return;
        }
        //创建任务实例
        JobInstance jobInstance=new JobInstance();
        jobInstance.setJobId(trigger.getJobId());
        jobInstance.setWorkGroup(jobDetail.getWorkGroup());
        jobInstance.setJobHandler(jobDetail.getJobHandler());
        jobInstance.setFireTime(trigger.getNextFireTime());
        jobInstance.setRetryCount(jobDetail.getRetryCount());
        jobInstance.setInstanceState(JobInstanceStateEnum.INIT.getCode());

        jobInstanceMapper.insert(jobInstance);

        //构建触发参数
        long delay=TimeUtils.calculateTimeSpan(TimeUtils.nowDate(), trigger.getNextFireTime()).toMillis();
        if(delay < 0L){
            delay=0L;
        }
        TriggerParameter triggerParameter=new TriggerParameter();
        triggerParameter.setJobId(trigger.getJobId());
        triggerParameter.setFireTime(trigger.getNextFireTime().getTime());
        triggerParameter.setDelay(delay);
        triggerParameter.setJobId(trigger.getJobId());
        triggerParameter.setInstanceId(jobInstance.getInstanceId());
        triggerParameter.setWorkGroup(jobDetail.getWorkGroup());
        triggerParameter.setWorkerRouteStrategy(jobDetail.getWorkerRouteStrategy());
        triggerParameter.setJobHandler(jobDetail.getJobHandler());
        triggerParameter.setJobUpdateTime(jobDetail.getUpdateTime());

        final List<JobDetailExtend> exts = jobDetailExtendMapper.listByJobId(trigger.getJobId());
        final JobDetailExtends jobDetailExtends=new JobDetailExtends();
        exts.forEach(jobDetailExtend -> {
            jobDetailExtends.put(jobDetailExtend.getExtKey(),jobDetailExtend.getExtValue());
        });
        triggerParameter.setJobDetailExtends(jobDetailExtends);

        if(delay<1000L){
            delay=0L;
        }else if(delay > 3000L){
            delay -=3000L;
        }else {
            delay=1000L;
        }
        destinyTimingWheelScheduler.newTimeout(triggerParameter,delay, TimeUnit.MILLISECONDS);
    }
}
